#-*- coding: utf-8 -*-

# (BSD 2-clause)
# Copyright (c) 2012, Mario Hock, Michael Tänzer, Karin Schmidt.
# All rights reserved.
# 
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY THE INSTITUTE OF TELEMATICS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE ARIBA PROJECT OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation
# are those of the authors and should not be interpreted as representing
# official policies, either expressed or implied, of the copyright holders.

'''
This module provides bindings to connect to the Omnibus Filetransfer backend 
'''

import socket as libsock
import time
import xml.etree.ElementTree as etree
from sys import stderr
from traceback import format_exc
import errno
import os
import weakref

class EVENT_TYPE(object):
    '''
    Event type constants
    
    Keyword arguments for the handlers:
    `USER_LIST`
        - users: a list of `User` objects
    
    `CHAT_MSG`
        - sender
        - receiver
        - message_id
        - time_stamp
        - message
    
    `CHAT_ECHO`
        - sender
        - receiver
        - message_id
        - time_stamp
        - message
    
    `CHAT_ACK`
        - chat_to: the user the acked message was sent to
        - message_id: the id of the acked message
    '''
    #TODO: document, document, document
    
    ALL = ''
    
    ONLINE_STATUS = 'online_status'
    
    USER = 'user'
    USER_LIST = 'user.list'
    USER_UPDATE = 'user.update'
    USER_MYSHARES = 'user.myshares'
    USER_OTHERSHARES = 'user.othershares'
    
    TRANSFER = 'transfer'
    TRANSFER_LIST = 'transfer.list'
    TRANSFER_UPDATE = 'transfer.update'
    
    CHAT = 'chat'
    CHAT_MSG = 'chat.msg'
    CHAT_ECHO = 'chat.echo'
    CHAT_ACK = 'chat.ack'
    
    ERROR = 'error'
    ERROR_CONNECTION = 'error.connection'
    ERROR_CONNECTION_RECEIVE = 'error.connection.receive'
    ERROR_CONNECTION_SEND = 'error.connection.send'
    ERROR_UNKNOWNCOMMAND = 'error.unknowncommand'
    
    INFO = 'info'
    WARN = 'warn'
    
    # Exceptions
    class UnknownEventTypeError(ValueError):
        pass


class User(object):
    '''
    Represents a user in the user list
    '''
    
    class USERSTATUS(object):
        '''User status constants'''
        DISCONNECTED = 0
        CONNECTED = 1
        CONNECTING = 2
        DISCONNECTING = 3
        AUTHENTICATION_PENDING = 4,
        SEND_ESTABLISHED = 5,
        RECEIVE_ESTABLISHED = 6,
        CONNECTION_ERROR = 7,
        ERROR_NOT_CONNECTED = 8  # means, there was an error before (maybe it's resolved now.)

        TRANSITIONS = (2, 3, 4, 5, 6)
        ERRORS = (7, 8)        
        
        # Exceptions
        class UnknownConnectionStatusError(ValueError):
            pass
    
    def __init__(self, user_element):
        self.name = user_element.findtext('name')
        self.id = user_element.findtext('id')
        self.connected = int(user_element.findtext('connected'))
        if self.connected < 0  or self.connected > 8:
            raise User.USERSTATUS.UnknownConnectionStatusError()

class Filetransfer(object):
    '''
    Represents a filetransfer
    '''
    
    def __init__(self, file_element):
        self.key = file_element.findtext('key')
        self.filename = file_element.findtext('filename')
        self.size = int(file_element.findtext('size'))
        self.partner = file_element.findtext('partner')
        self.state = file_element.findtext('state')
        self.transferred = int(file_element.findtext('transferred'))
        self.time_consumed = int(file_element.findtext('time_consumed'))


class Share(object):
    '''
    Represents a file shared with a user
    '''
    
    def __init__(self, file_element):
        self.path = file_element.findtext('path')
        self.privileges = int(file_element.findtext('priv'))


class OfferedFile(object):
    '''
    Represents a file shared with us
    '''
    
    def __init__(self, connection, partner, file_element):
        assert isinstance(file_element, etree.Element)
        self._connection = connection
        self.partner = partner
        self.name = file_element.findtext('name')
        self._size = int(file_element.findtext('size'))
    
    def size(self):
        '''File size in bytes'''
        return self._size
    
    def pull(self):
        self._connection.pull(self.partner, self.name)


class OfferedDir(object):
    '''
    Represents a directory shared with us
    '''
    
    def __init__(self, connection, partner, parent=None):
        self._connection = connection
        self.partner = partner
        self.name = str()
        self.parent = parent
    
    def size(self):
        pass
    
    def refresh(self):
        self._connection.look(self.partner, self.name)
    
    def pull(self):
        self._connection.pullsync(self.partner, self.name)


class OfferedConcreteDir(OfferedDir):
    '''
    Represents a directory of which we already have the contents
    '''
    
    _dir_cache = weakref.WeakValueDictionary()
    
    def __init__(self, connection, partner, dir_element, parent=None):
        assert isinstance(dir_element, etree.Element)
        
        # stores the previous version of the directory
        previous = None
        
        name = dir_element.findtext('name', default='')
        
        # if there already is a parent_name directory, update it
        parent_name = os.path.dirname(name)
        base_name = os.path.basename(name)
        if base_name != '' and not parent and \
                (partner, parent_name) in OfferedConcreteDir._dir_cache:
            parent = OfferedConcreteDir._dir_cache[(partner, parent_name)]
            previous = parent[base_name]
            parent[base_name] = self
        
        if not previous and \
                (partner, name) in OfferedConcreteDir._dir_cache:
            previous = OfferedConcreteDir._dir_cache[(partner, name)]
        
        # copy any values on the cached item so custom attributes may be
        # attached on the object without loosing them on the update
        if previous:
            self.__dict__.update(previous.__dict__)
        
        super(OfferedConcreteDir, self).__init__(connection, partner, parent)
        self.name = name
        
        self._subelements = dict()
        
        for element in dir_element:
            assert isinstance(element, etree.Element)
            
            if element.tag == 'subdir':
                subelement = OfferedDummyDir(self._connection,
                                             self.partner,
                                             element,
                                             self)
            elif element.tag == 'file':
                subelement = OfferedFile(self._connection,
                                         self.partner,
                                         element)
            elif element.tag == 'dir':
                subelement = OfferedConcreteDir(self._connection,
                                                self.partner,
                                                element,
                                                self)
            else:
                continue
            
            if self.name == '':
                self._subelements[subelement.name] = subelement
            else:
                self._subelements[os.path.basename(subelement.name)] = subelement
        
        OfferedConcreteDir._dir_cache[(self.partner, self.name)] = self
    
    def size(self):
        return len(self._subelements)
    
    def __len__(self):
        return len(self._subelements)
    
    def __contains__(self, item):
        return item in self._subelements
    
    def __iter__(self):
        return iter(self._subelements)
    
    def __reversed__(self):
        return reversed(self._subelements)
    
    def __getitem__(self, key):
        return self._subelements[key]
    
    def __setitem__(self, key, value):
        self._subelements[key] = value
    
    def __delitem__(self, key):
        del self._subelements[key]
    

class OfferedDummyDir(OfferedDir):
    '''
    Represents a directory of which we don't have the contents yet
    '''
    def __init__(self, connection, partner, subdir_element, parent=None):
        assert isinstance(subdir_element, etree.Element)
        super(OfferedDummyDir, self).__init__(connection, partner, parent)
        
        self.name = subdir_element.findtext('name')
        self._size = int(subdir_element.findtext('size'))
    
    def size(self):
        '''Number of contained items'''
        return self._size


class Connection(object):
    '''
    This class represents a connection to the Omnibus Filetransfer backend
    '''
    
    class NoConnectionError(Exception):
        pass
    
    
    def __init__(self, socket=None, unix_path=None, tcp_address=None):
        '''
        One of the keyword arguments `socket`, `unix_path` or `tcp_address`
        has to be provided
        
        `socket`
            will be used for communicating with the backend. It has to be in
            connected state
        
        `unix_path`
            if `socket` is not specified and a `socket_path` is given and UNIX
            socket support is available a UNIX socket for that path is created
        
        `tcp_address`
            if `socket_path` is not specified and a `tcp_address` is given
            (has to be a ``(host, port)`` tuple or on IPv6 it may also be a
            ``(host, port, flowinfo, scopeid)`` tuple). Host may be an DNS name,
            IPv4 or IPv6 address
        '''
        if socket:
            self._socket = socket
            
        else:
            unix_support = False
            
            if unix_path:
                # is libsock.AF_UNIX defined => UNIX socket support available?
                try:
                    libsock.AF_UNIX
                except NameError:
                    unix_support = False
                else:
                    unix_support = True
            
            if unix_support:
                sock = libsock.socket(libsock.AF_UNIX)
                
                # wait for connection if it can't be established yet
                tries = 5
                while True:
                    try:
                        sock.connect(unix_path)
                        self._socket = sock
                        break
                    
                    except libsock.error as e:
                        if e.errno not in (errno.ENOENT, errno.ECONNREFUSED):
                            # re-raise exception if wrong type
                            e.filename = unix_path
                            raise e
                        
                        elif tries > 0:
                            tries -= 1
                            time.sleep(0.5)
                            
                        else:
                            # re-raise exception if too many unsuccessful tries
                            e.filename = unix_path
                            print >> stderr, "ERROR: Can't connect to "+\
                                    "Omnibus-Backend. Giving up."
                            raise Connection.NoConnectionError(e)
                
            elif tcp_address:
                self._socket = libsock.create_connection(tcp_address)
                
            else:
                raise ValueError("Please specify one of the keyword arguments "+
                                 "so a proper socket can be created")
        
        self._socket.setblocking(0)
        
        self._handlers = dict()
        self._received = ''
    
    
    def close(self):
        '''
        Close the underlying socket. Will also be closed when garbage-collected
        but closing it explicitly completely avoids file descriptor leak.
        
        The connection will be closed after queued data has been sent. To
        immediately shutdown the connection without sending already queued data
        first, use `abort()`
        '''
        try:
            self._socket.close()
        except libsock.error:
            pass
    
    
    def abort(self):
        '''
        Immediately close the underlying socket without sending out remaining
        data.
        '''
        try:
            self._socket.shutdown(libsock.SHUT_RDWR)
        except libsock.error:
            pass
        
        try:
            self._socket.close()
        except libsock.error:
            pass
    
    
    def quit_backend(self):
        '''
        Tell the backend to quit and close the connection
        '''
        self._legacy_send(u'quit')
        self.close()
    
    
    def get_socket(self):
        '''
        Return the underlying socket. This is useful for registering it with
        functions that check multiple input sources for available data.
        E.g. `glib.io_add_watch()` or `select.select()`
        Use `read_handler()` as callback when new data is available for
        reading  
        '''
        return self._socket
    
    
    def read_handler(self, *_):
        '''
        Process one chunk of data that is available for reading.
        '''
        while True:
            try:
                piece = self._socket.recv(4096)
            except libsock.error as e:
                if e.errno == errno.EAGAIN:
                    break # error raised because of non-blocking mode and
                          # no more data available on the socket
                
                self.abort()
                self._error_event(str(e),
                                  EVENT_TYPE.ERROR_CONNECTION_RECEIVE,
                                  EVENT_TYPE.ERROR_CONNECTION)
                return True
            
            self._received += piece
            
            if len(piece) < 4096:
                break # read was short -> no more data available on the socket
        
        commands = self._received.split('<>')
        for command in commands[:-1]:
            command = command.strip()
            if command == "":
                raise Exception('Empty command received')
            
            if command.startswith("<"): # handle XML commands
                tree = etree.fromstring(command)
                
                func_string = '_%s_handler' % tree.tag
                
                def unknown_command_handler(tree):
                    self._error_event('Unknown command "%s"' % tree.tag,
                                      EVENT_TYPE.ERROR_UNKNOWNCOMMAND)
                
                try:
                    getattr(self, func_string, unknown_command_handler)(tree)
                except Exception as e:
                    self._error_event('Caught exception in event handler:\n' +
                                      format_exc())
                
            else: # handle legacy command
                handlers = [('[FT_UP]', self._filetransfer_update),
                            ('[USER_UP]', self._userstatus_update),
                            ('[ONLINE]', self._online_status_update)]
                for tag, handler in handlers:
                    if command.startswith(tag):
                        handler(command[len(tag):])
                        break
                else:
                    self._error_event('Unknown command "%s"' % command,
                                      EVENT_TYPE.ERROR_UNKNOWNCOMMAND)
        
        self._received = commands[-1]
        return True
    
    
    def register_event_handler(self, handler, event=EVENT_TYPE.ALL):
        '''
        Register a callback function that is called when an event of the
        specified type is triggered. The signature of the callback function
        has to be ::
        
            def callback(omnibus_connection, event_type, **kwargs)
        
        omnibus_connection
            the `Connection` which triggered the event
        
        event_type
            the type of event
        
        kwargs
            the keyword parameters specific to that event, see the documentation
            on `EVENT_TYPE` for details. ``**_`` should be specified after the
            last keyword argument so if new arguments are added, the application
            does not crash.
        '''
        event_handlers = self._handlers.get(event)
        if not event_handlers:
            event_handlers = set()
            self._handlers[event] = event_handlers
        
        event_handlers.add(handler)
    
    
    def unregister_event_handler(self, handler, event=EVENT_TYPE.ALL):
        '''
        Delete the registration of the callback function for the specified
        event types.
        
        Returns whether the handler was registered and successfully removed
        (False if the handler has not been registered previously)
        '''
        event_handlers = self._handlers.get(event, set())
        try:
            event_handlers.remove(handler)
        except KeyError:
            return False # ignore if the hadler was not registered
        
        return True 
    
    
    def _call_event_handlers(self, *eventlist, **handler_args):
        '''
        Successively check if there are any event handlers for the given events
        and call them. If there are handlers on the first event type the
        handlers for the following types will not be called
        '''
        for event in eventlist:
            event_handlers = self._handlers.get(event)
            
            if event_handlers:
                for handler in event_handlers:
                    handler(self, event, **handler_args)
                return
    
    
    
    #===========================================================================
    # Omnibus commands
    #===========================================================================
    def request_online_status(self):
        '''
        Request infromation about whether we are connected to the overlay
        '''
        self._legacy_send(u'ONLN')
    
    
    def meet(self, meet_phrase):
        '''
        Add another user to your "buddy list" by entering the same phrase
        '''
        self._legacy_send(u'MEET %s' % meet_phrase)
    
    
    def list_users(self):
        '''
        Request a list of users. Register a handler on EVENT_TYPE.USER_LIST
        '''
        self._legacy_send(u'LIST')
    
    
    def connect(self, user):
        '''
        Establish connection to `user`
        '''
        self._legacy_send(u'CONN %s' % user)
    
    
    def disconnect(self, user):
        '''
        Close connection to `user`
        '''
        self._legacy_send(u'DISC %s' % user)
    
    
    def custody(self, server, recipient, filename):
        '''
        Send the file referenced by `filename` via `server` to the `recipient`
        '''
        self._legacy_send(u'CSDY %s %s %s' % (server, recipient, filename))
    
    
    def push(self, recipient, path):
        '''
        Send the file or directory referenced by `path` to the `recipient`
        '''
        if os.path.isdir(path):
            pushsync = etree.Element('pushsync')
            etree.SubElement(pushsync, 'name').text = recipient
            etree.SubElement(pushsync, 'path').text = path
            
            self._send(pushsync)
        else:
            self._legacy_send(u'PUSH %s %s' % (recipient, path))
    
    
    def pull(self, sender, filename):
        '''
        Get the file referenced by `filename` shared with us by `sender`
        '''
        self._legacy_send(u'PULL %s %s' % (sender, filename))
    
    
    def pullsync(self, sender, path):
        '''
        Get the directory referenced by `path` shared with us by `sender`
        '''
        pullsync = etree.Element('pullsync')
        etree.SubElement(pullsync, 'name').text = sender
        etree.SubElement(pullsync, 'path').text = path
        
        self._send(pullsync)
    
    
    def stat(self):
        '''
        Request a list of running file transfers
        '''
        self._legacy_send(u'STAT')
    
    
    def accept_transfer(self, transfer_id):
        '''
        Accept incoming transfer referred to by `transfer_id`
        '''
        self._legacy_send(u'ACPT %s' % transfer_id)
    
    
    def pause_transfer(self, transfer_id):
        '''
        Pause the incoming transfer referred to by `transfer_id`
        '''
        self._legacy_send(u'WAIT %s' % transfer_id)
    
    
    def resume_transfer(self, transfer_id):
        '''
        Resume a paused incoming transfer referred to by `transfer_id`
        '''
        self._legacy_send(u'GOON %s' % transfer_id)
    
    
    def stop_transfer(self, transfer_id):
        '''
        Stop the outgoing transfer referred to by `transfer_id`
        '''
        self._legacy_send(u'STOP %s' % transfer_id)
    
    
    def continue_transfer(self, transfer_id):
        '''
        Continue a stopped outgoing transfer referred to by `transfer_id`
        '''
        self._legacy_send(u'CONT %s' % transfer_id)
    
    
    def decline_transfer(self, transfer_id):
        '''
        Decline incoming transfer referred to by `transfer_id`
        '''
        self._legacy_send(u'DECL %s' % transfer_id)
    
    
    def abort_transfer(self, transfer_id):
        '''
        Abort outgoing transfer referred to by `transfer_id`
        '''
        self._legacy_send(u'ABRT %s' % transfer_id)
    
    
    def pause_all_transfers(self):
        '''
        Pause all running transfers (incoming and outgoing)
        '''
        self._legacy_send(u'PALL')
    
    
    def open_incoming(self, transfer_id='', user=''):
        '''
        Open the file/folder that was downloaded
        
        `transfer_id`
            The ID of the transfer which should be opened
        
        `user`
            If `transfer_id` was not given the incoming directory of the `user`
            is opened
        
        If neither `transfer_id` nor `user` are given, the parent directory of
        all incoming directories is opened.
        
        NOTE: Currently neither `transfer_id` nor `user` are currently
        implemented
        '''
        _ = transfer_id
        _ = user
        self._legacy_send(u'OINC')
    
    
    def look(self, sender, path=''):
        '''
        Show the files under `path` that `sender` has shared with us
        '''
        self._legacy_send(u'LOOK %s %s' % (sender, path))
    
    
    def show(self, recipient):
        '''
        Show the files that we have shared with the `recipient`
        '''
        self._legacy_send(u'SHOW %s' % recipient)
    
    
    def give(self, recipient, path):
        '''
        Add `path` to the files that we have shared with the `recipient`
        '''
        self._legacy_send(u'GIVE %s %s' % (recipient, path))
    
    
    def chat(self, recipient, text):
        '''
        Send a chat message containing `text` to `recipient`
        '''
        chat = etree.Element('chat')
        
        etree.SubElement(chat, 'to').text = recipient
        etree.SubElement(chat, 'text').text = text
        
        self._send(chat)
    
    
    
    #===========================================================================
    # Internal stuff
    #===========================================================================
    def _online_status_update(self, command):
        online = command.strip().startswith('true')
        
        self._call_event_handlers(EVENT_TYPE.ONLINE_STATUS,
                                  EVENT_TYPE.ALL,
                                  
                                  online_status=online)
    
    
    def _users_handler(self, tree):
        users = list()
        for user_element in tree.findall('user'):
            users.append(User(user_element))
        
        self._call_event_handlers(EVENT_TYPE.USER_LIST,
                                  EVENT_TYPE.USER,
                                  EVENT_TYPE.ALL,
                                  
                                  users=users)
    
    
    def _userstatus_update(self, command):
        _ = command
        self._call_event_handlers(EVENT_TYPE.USER_UPDATE,
                                  EVENT_TYPE.USER,
                                  EVENT_TYPE.ALL)
    
    
    def _filetransfers_handler(self, tree):
        queues = {'incoming':list(), 'outgoing':list(), 'finished':list()}
        for queue in queues:
            queue_element = tree.find(queue)
            for file_element in queue_element.findall('file'):
                queues[queue].append(Filetransfer(file_element))
        
        self._call_event_handlers(EVENT_TYPE.TRANSFER_LIST,
                                  EVENT_TYPE.TRANSFER,
                                  EVENT_TYPE.ALL,
                                  
                                  incoming=queues['incoming'],
                                  outgoing=queues['outgoing'],
                                  finished=queues['finished'])
    
    
    def _accessible_files_handler(self, tree):
        sender = tree.findtext('partner')
        offered_dir = OfferedConcreteDir(self, sender, tree)
        
        self._call_event_handlers(EVENT_TYPE.USER_OTHERSHARES,
                                  EVENT_TYPE.USER,
                                  EVENT_TYPE.ALL,
                                  
                                  sender=sender,
                                  offers=offered_dir)
    
    
    def _access_handler(self, tree):
        receiver = tree.findtext('partner')
        
        shares = list()
        for file_element in tree.findall('file'):
            shares.append(Share(file_element))
        
        self._call_event_handlers(EVENT_TYPE.USER_MYSHARES,
                                  EVENT_TYPE.USER,
                                  EVENT_TYPE.ALL,
                                  
                                  receiver=receiver,
                                  shares=shares)
    
    
    def _filetransfer_update(self, command):
        _ = command
        self._call_event_handlers(EVENT_TYPE.TRANSFER_UPDATE,
                                  EVENT_TYPE.TRANSFER,
                                  EVENT_TYPE.ALL)
    
    
    def _chat_handler(self, tree):
        message = tree.find('text').text
        if not message:
            message = ''
        
        self._call_event_handlers(EVENT_TYPE.CHAT_MSG,
                                  EVENT_TYPE.CHAT,
                                  EVENT_TYPE.ALL,
                                  
                                  sender=tree.find('from').text,
                                  receiver=tree.find('to').text,
                                  message_id=tree.find('id').text,
                                  time_stamp=tree.find('time_stamp').text,
                                  message=message)
    
    
    def _chatecho_handler(self, tree):
        message = tree.find('text').text
        if not message:
            message = ''
        
        self._call_event_handlers(EVENT_TYPE.CHAT_ECHO,
                                  EVENT_TYPE.CHAT,
                                  EVENT_TYPE.ALL,
                                  
                                  sender=tree.find('from').text,
                                  receiver=tree.find('to').text,
                                  message_id=tree.find('id').text,
                                  time_stamp=tree.find('time_stamp').text,
                                  message=message)
    
    
    def _chatack_handler(self, tree):
        self._call_event_handlers(EVENT_TYPE.CHAT_ACK,
                                  EVENT_TYPE.CHAT,
                                  EVENT_TYPE.ALL,
                                  
                                  chat_to=tree.find('chat_to').text,
                                  message_id=tree.find('id').text)
    
    
    def _info_handler(self, tree):
        self._call_event_handlers(EVENT_TYPE.INFO,
                                  EVENT_TYPE.ALL,
                                  
                                  message=tree.text)
        
    def _warn_handler(self, tree):
        self._call_event_handlers(EVENT_TYPE.WARN,
                                  EVENT_TYPE.INFO,
                                  EVENT_TYPE.ALL,
                                  
                                  message=tree.text)

    
    
    def _error_event(self, message, *events):
        events = list(events)
        events.append(EVENT_TYPE.ERROR)
        events.append(EVENT_TYPE.ALL)
        
        self._call_event_handlers(*events,
                                  
                                  message=message)
    
    
    def _send(self, command_tree):
        command_string = etree.tostring(command_tree, 'ascii') + "<>"
        try:
            self._socket.send(command_string)
        except libsock.error as e:
            if e.errno == errno.EAGAIN:
                # retry with a short time out
                self._socket.settimeout(1)
                try:
                    self._socket.send(command_string)
                except libsock.error as e:
                    self.abort()
                    self._error_event(str(e),
                                      EVENT_TYPE.ERROR_CONNECTION_SEND,
                                      EVENT_TYPE.ERROR_CONNECTION)
                    return
                
                # retry was successful set back to non-blocking mode
                self._socket.setblocking(0)
            
            self.abort()
            self._error_event(str(e),
                              EVENT_TYPE.ERROR_CONNECTION_SEND,
                              EVENT_TYPE.ERROR_CONNECTION)
            return
    
    
    def _legacy_send(self, command):
        legacy = etree.Element('legacy')
        legacy.text = command
        
        self._send(legacy)
